/*
 * JSF Toolkit Component Framework
 * Copyright (C) 2007 Noah Sloan <iamnoah A-T gmail D0T com>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 */
package com.jsftoolkit.utils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.PushbackReader;

/**
 * A DelimitedReader wraps another Reader and allows reading from that Reader,
 * up to a given delimeter (a byte sequence).
 * <p>
 * It is useful for wrapping Readers that you want to pass to a Scanner or other
 * buffering reader but that will need to be accessed directly later. i.e. you
 * need a Scanner but aren't willing to give up all the data.
 * <p>
 * This class will <em>never</em> buffer ahead, so it may be desirable to wrap
 * the passed in reader in a {@link BufferedReader} before handing it to this
 * class.
 * <p>
 * The matched delimeter can be obtained by calling {@link #getMatch()} after
 * EOS is returned.
 * 
 * @author noah
 * 
 */
public class DelimitedReader extends Reader {

	private final String delimeter; // the delimeter to look for

	private final PushbackReader reader; // stream to read bytes from

	private final boolean[] mask;

	private char[] match = null;

	/**
	 * 
	 * @param in
	 * @param delimeter
	 */
	public DelimitedReader(Reader in, String delimeter) {
		this(new PushbackReader(in, delimeter.length()), delimeter);
	}

	/**
	 * Assumes the given delimeter should be matched exactly.
	 * 
	 * @param in
	 * @param delimeter
	 */
	public DelimitedReader(PushbackReader in, String delimeter) {
		this(in, delimeter, Utils.fill(new boolean[delimeter.length()], true));
	}

	/**
	 * 
	 * @param reader
	 *            a {@link PushbackReader}. Note that the pushback buffer must
	 *            be at least as long as delimeter - 1
	 * @param delimeter
	 *            the delimeter to halt after
	 * @param mask
	 *            the delimeter mask. Indicates if the char at the given
	 *            position should be matched (true) or not matched (false).
	 */
	public DelimitedReader(PushbackReader in, String delimeter, boolean[] mask) {
		this.reader = in;
		this.delimeter = delimeter;
		this.mask = mask;
	}

	/**
	 * Wraps the reader in a {@link PushbackReader} and calls
	 * {@link #DelimitedReader(PushbackReader, String, boolean[])}.
	 * 
	 * @param in
	 * @param delimeter
	 * @param mask
	 */
	public DelimitedReader(Reader in, String delimeter, boolean[] mask) {
		this(new PushbackReader(in, delimeter.length()), delimeter, mask);
	}

	@Override
	/**
	 * Obeys the general contract of read. Returns -1 at end of stream.
	 */
	public int read() throws IOException {
		if (match == null) {
			int read = reader.read();
			if (read == -1) {
				match = new char[0];
			} else if (matches(read, 0)) {
				// if the char matches the first character of the delimeter, see
				// if the whole delimeter is there
				int[] buf = new int[delimeter.length()];
				buf[0] = read;
				int i = 1;
				while (i < delimeter.length()
						&& matches((buf[i] = reader.read()), i)) {
					i++;
				}
				if (i == delimeter.length()) {
					// matched, so return EOS
					match = Utils.toCharArray(buf);
					return -1;
				} else {
					// not a match, so we unread the extra chars
					for (int j = i; j > 0; j--) {
						reader.unread(buf[j]);
					}
				}
			}
			return read;
		}
		return -1;
	}

	protected boolean matches(int b, int index) {
		return mask[index] ? delimeter.charAt(index) == b : b != -1
				&& delimeter.charAt(index) != b;
	}

	@Override
	/**
	 * Reads up to the delimeter or EOS. In other words, consumes all the bytes
	 * up to and including the delimeter. Does not close the underlying stream.
	 */
	public void close() throws IOException {
		while (read() != -1)
			;
	}

	/**
	 * Returns the bytes the matched the delimeter (useful when you specify a
	 * mask that is not all matching).
	 * 
	 * This method returns null if the EOS or delimeter has not been reached. It
	 * returns an empty array if the delimeter was not matched. Note that EOS
	 * will not match anything, even if the mask bit for the last character is
	 * set to not match. i.e. If only one more character is needed to match the
	 * delimeter but the EOS is reached, then this method returns an empty
	 * array.
	 * 
	 * @return
	 */
	public char[] getMatch() {
		return match;
	}

	/**
	 * Returns the wrapped reader. When this ({@link DelimitedReader}) reader
	 * reaches the end of stream, the next character returned by the wrapped
	 * reader will be the character immediately following the delimeter. Note
	 * that this reader may be different from the one passed into the
	 * constructor.
	 * 
	 * @return the wrapped reader.
	 */
	public PushbackReader getReader() {
		return reader;
	}

	@Override
	public int read(char[] cbuf, int off, int len) throws IOException {
		int i;
		int read = -1;
		for (i = 0; i < len && (read = read()) != -1; i++) {
			cbuf[i + off] = (char) read;
		}
		return read == -1 && i == 0 ? -1 : i;
	}
}